/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "core_main.h"
#include "ZLib/unzip.h"


#define FILES_HASH_SIZE			1024

#define MAX_FILE_HANDLES		32

#define MAX_LIST_FILES			65536
#define MAX_LIST_MODS			128

typedef struct {
	char					name[MAX_OSPATH];
	fsMode_t				mode;

	FILE *					realFile;				// Only one of realFile or
	unzFile					zipFile;				// zipFile will be used
} file_t;

typedef struct fileInPack_s {
	char					name[MAX_OSPATH];
	bool					isDirectory;

	int						size;
	int						offset;

	struct fileInPack_s *	nextHash;
} fileInPack_t;

typedef struct {
	char					name[MAX_OSPATH];
	bool					pure;

	unzFile					handle;
	ulong					checksum;

	int						numFiles;
	fileInPack_t *			files;
	fileInPack_t *			filesHashTable[FILES_HASH_SIZE];
} pack_t;

typedef struct {
	char					base[MAX_OSPATH];
	char					game[MAX_OSPATH];
} directory_t;

typedef struct searchPath_s {
	pack_t *				pack;					// Only one of pack or
	directory_t *			directory;				// directory will be used

	struct searchPath_s *	next;
} searchPath_t;

static file_t				fs_fileHandles[MAX_FILE_HANDLES];

static searchPath_t *		fs_searchPaths;

static bool					fs_pure;

static bool					fs_purePackChecksumsValid;
static ulong				fs_purePackChecksums[MAX_PACK_FILES];

static cvar_t *				fs_basePath;
static cvar_t *				fs_savePath;
static cvar_t *				fs_baseGame;
static cvar_t *				fs_game;
static cvar_t *				fs_debug;


/*
 ==================
 FS_HandleForFile

 Finds a free fileHandle_t
 ==================
*/
static file_t *FS_HandleForFile (fileHandle_t *f){

	file_t	*file;
	int		i;

	for (i = 0, file = fs_fileHandles; i < MAX_FILE_HANDLES; i++, file++){
		if (!file->realFile && !file->zipFile)
			break;
	}

	if (i == MAX_FILE_HANDLES)
		Com_Error(true, "FS_HandleForFile: none free");

	*f = i+1;

	return file;
}

/*
 ==================
 FS_GetFileByHandle

 Returns a file_t for the given fileHandle_t
 ==================
*/
static file_t *FS_GetFileByHandle (fileHandle_t f){

	file_t	*file;

	if (f <= 0 || f > MAX_FILE_HANDLES)
		Com_Error(true, "FS_GetFileByHandle: handle out of range");

	file = &fs_fileHandles[f-1];

	if (!file->realFile && !file->zipFile)
		Com_Error(true, "FS_GetFileByHandle: invalid handle");

	return file;
}

/*
 ==================
 FS_FileLength

 Returns the length of the given FILE
 ==================
*/
static int FS_FileLength (FILE *f){

	int		cur, end;

	cur = ftell(f);
	fseek(f, 0, SEEK_END);
	end = ftell(f);
	fseek(f, cur, SEEK_SET);

	return end;
}

/*
 ==================
 FS_BuildOSPath

 Builds a full OS path from the given components
 ==================
*/
static void FS_BuildOSPath (char *path, int pathSize, const char *base, const char *game, const char *name){

	Str_SPrintf(path, pathSize, "%s/%s/%s", base, game, name);

	// Fix up path separators
	while (*path){
		if (*path == '/' || *path == '\\')
			*path = PATH_SEPARATOR_CHAR;

		path++;
	}
}

/*
 ==================
 FS_CreateOSPath

 Creates the given OS path if it doesn't exist
 ==================
*/
static void FS_CreateOSPath (char *path){

	char	*p;

	p = path + 1;

	while (*p){
		if (*p == PATH_SEPARATOR_CHAR){
			*p = 0;

			Sys_CreateDirectory(path);

			*p = PATH_SEPARATOR_CHAR;
		}

		p++;
	}
}

/*
 ==================
 FS_ValidateFileName

 Returns true if the given file name is valid
 ==================
*/
static bool FS_ValidateFileName (const char *name){

	if (!name || !name[0])
		Com_Error(true, "FS_ValidateFileName: NULL file name");

	if (Str_Length(name) >= MAX_OSPATH)
		Com_Error(true, "FS_ValidateFileName: file name exceeds MAX_OSPATH");

	// Make sure it doesn't contain wildcards or spaces
	if (Str_FindChar(name, '*') || Str_FindChar(name, '?') || Str_FindChar(name, ' ')){
		Com_DPrintf(S_COLOR_RED "FS_ValidateFileName: illegal file name '%s'\n", name);
		return false;
	}

	// Make absolutely sure that it can't back up the path
	if (Str_FindText(name, "..", false) || Str_FindText(name, "::", false)){
		Com_DPrintf(S_COLOR_RED "FS_ValidateFileName: illegal file name '%s'\n", name);
		return false;
	}

	return true;
}

/*
 ==================
 FS_FileNameCompare

 Ignores case and separator char distinctions
 ==================
*/
static int FS_FileNameCompare (const char *name1, const char *name2){

	int		c1, c2;

	do {
		c1 = *name1++;
		c2 = *name2++;

		if (c1 != c2){
			if (c1 >= 'a' && c1 <= 'z')
				c1 -= ('a' - 'A');
			if (c2 >= 'a' && c2 <= 'z')
				c2 -= ('a' - 'A');

			if (c1 == '\\' || c1 == ':')
				c1 = '/';
			if (c2 == '\\' || c2 == ':')
				c2 = '/';

			if (c1 != c2)
				return c1 < c2 ? -1 : 1;	// Names not equal
		}
	} while (c1);

	return 0;	// Names are equal
}


/*
 ==============================================================================

 FILE I/O FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 FS_Read

 Properly handles partial reads
 ==================
*/
int FS_Read (void *buffer, int size, fileHandle_t f){

	file_t	*file;
	byte	*ptr;
	int		remaining, count;

	if (!buffer)
		Com_Error(true, "FS_Read: NULL buffer");
	if (size < 0)
		Com_Error(true, "FS_Read: size < 0");

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_READ)
		Com_Error(true, "FS_Read: can't read from '%s'", file->name);

	ptr = (byte *)buffer;
	remaining = size;

	while (remaining){
		if (file->realFile)
			count = fread(ptr, 1, remaining, file->realFile);
		else if (file->zipFile)
			count = unzReadCurrentFile(file->zipFile, ptr, remaining);

		if (count == 0){
			Com_DPrintf(S_COLOR_RED "FS_Read: 0 bytes read from '%s'\n", file->name);
			return size - remaining;
		}

		if (count == -1)
			Com_Error(true, "FS_Read: -1 bytes read from '%s'", file->name);

		ptr += count;
		remaining -= count;
	}

	return size;
}

/*
 ==================
 FS_Write

 Properly handles partial writes
 ==================
*/
int FS_Write (const void *buffer, int size, fileHandle_t f){

	file_t	*file;
	byte	*ptr;
	int		remaining, count;

	if (!buffer)
		Com_Error(true, "FS_Write: NULL buffer");
	if (size < 0)
		Com_Error(true, "FS_Write: size < 0");

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_WRITE && file->mode != FS_APPEND)
		Com_Error(true, "FS_Write: can't write to '%s'", file->name);

	ptr = (byte *)buffer;
	remaining = size;

	while (remaining){
		if (file->realFile)
			count = fwrite(ptr, 1, remaining, file->realFile);
		else if (file->zipFile)
			Com_Error(true, "FS_Write: can't write to zipped file '%s'", file->name);

		if (count == 0){
			Com_DPrintf(S_COLOR_RED "FS_Write: 0 bytes written to '%s'\n", file->name);
			return size - remaining;
		}

		if (count == -1)
			Com_Error(true, "FS_Write: -1 bytes written to '%s'", file->name);

		ptr += count;
		remaining -= count;
	}

	return size;
}

/*
 ==================
 FS_Printf
 ==================
*/
int FS_Printf (fileHandle_t f, const char *fmt, ...){

	file_t	*file;
	va_list	argPtr;
	int		count;

	file = FS_GetFileByHandle(f);

	if (file->mode != FS_WRITE && file->mode != FS_APPEND)
		Com_Error(true, "FS_Printf: can't write to '%s'", file->name);

	if (file->realFile){
		va_start(argPtr, fmt);
		count = vfprintf(file->realFile, fmt, argPtr);
		va_end(argPtr);
	}
	else if (file->zipFile)
		Com_Error(true, "FS_Printf: can't write to zipped file '%s'", file->name);

	if (count == 0){
		Com_DPrintf(S_COLOR_RED "FS_Printf: 0 chars written to '%s'\n", file->name);
		return 0;
	}

	if (count == -1)
		Com_Error(true, "FS_Printf: -1 chars written to '%s'", file->name);

	return count;
}

/*
 ==================
 FS_Seek
 ==================
*/
void FS_Seek (fileHandle_t f, int offset, fsOrigin_t origin){

	file_t			*file;
	unz_file_info	info;
	byte			dummy[0x8000];
	int				remaining, count;

	file = FS_GetFileByHandle(f);

	if (file->realFile){
		switch (origin){
		case FS_SEEK_SET:
			fseek(file->realFile, offset, SEEK_SET);
			break;
		case FS_SEEK_CUR:
			fseek(file->realFile, offset, SEEK_CUR);
			break;
		case FS_SEEK_END:
			fseek(file->realFile, offset, SEEK_END);
			break;
		default:
			Com_Error(true, "FS_Seek: bad origin for '%s'", file->name);
		}
	}
	else if (file->zipFile){
		switch (origin){
		case FS_SEEK_SET:
			remaining = offset;
			break;
		case FS_SEEK_CUR:
			remaining = offset + unztell(file->zipFile);
			break;
		case FS_SEEK_END:
			unzGetCurrentFileInfo(file->zipFile, &info, NULL, 0, NULL, 0, NULL, 0);

			remaining = offset + info.uncompressed_size;
			break;
		default:
			Com_Error(true, "FS_Seek: bad origin for '%s'", file->name);
		}

		// Reopen the file
		unzCloseCurrentFile(file->zipFile);

		if (unzOpenCurrentFile(file->zipFile) != UNZ_OK)
			return;

		// Skip until the desired offset is reached
		if (remaining <= 0)
			return;

		while (remaining){
			count = remaining;
			if (count > sizeof(dummy))
				count = sizeof(dummy);

			count = unzReadCurrentFile(file->zipFile, dummy, count);
			if (count <= 0)
				break;

			remaining -= count;
		}
	}
}

/*
 ==================
 FS_Tell
 ==================
*/
int FS_Tell (fileHandle_t f){

	file_t	*file;

	file = FS_GetFileByHandle(f);

	if (file->realFile)
		return ftell(file->realFile);
	else if (file->zipFile)
		return unztell(file->zipFile);

	return 0;
}

/*
 ==================
 FS_Flush
 ==================
*/
void FS_Flush (fileHandle_t f){

	file_t	*file;

	file = FS_GetFileByHandle(f);

	if (file->realFile)
		fflush(file->realFile);
	else if (file->zipFile)
		Com_Error(true, "FS_Flush: can't flush zipped file '%s'", file->name);
}

/*
 ==================
 FS_ForceFlush
 ==================
*/
void FS_ForceFlush (fileHandle_t f){

	file_t	*file;

	file = FS_GetFileByHandle(f);

	if (file->realFile)
		setvbuf(file->realFile, NULL, _IONBF, 0);
	else if (file->zipFile)
		Com_Error(true, "FS_ForceFlush: can't flush zipped file '%s'", file->name);
}


/*
 ==============================================================================

 FUNCTIONS FOR ENTIRE FILES

 ==============================================================================
*/


/*
 ==================
 FS_OpenFileRead
 ==================
*/
static int FS_OpenFileRead (const char *name, FILE **realFile, unzFile *zipFile){

	searchPath_t	*searchPath;
	pack_t			*pack;
	fileInPack_t	*fileInPack;
	directory_t		*directory;
	char			path[MAX_OSPATH];
	char			extension[16];
	uint			hashKey;

	// Special case for absolute paths to pack files
	Str_ExtractFileExtension(name, extension, sizeof(extension));

	if (!FS_FileNameCompare(extension, ".pk3")){
		// Open the file
		*realFile = fopen(name, "rb");
		if (*realFile){
			if (fs_debug->integerValue)
				Com_Printf("FS_OpenFileRead: '%s'\n", name);

			return FS_FileLength(*realFile);
		}

		if (fs_debug->integerValue)
			Com_Printf("FS_OpenFileRead: couldn't find '%s'\n", name);

		return -1;
	}

	// Make sure the file name is valid
	if (!FS_ValidateFileName(name))
		return -1;

	// Get the hash key for the given file name
	hashKey = Str_HashKey(name, FILES_HASH_SIZE, false);

	// Search through the paths, one element at a time
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		if (searchPath->pack){
			// If we are pure, don't search for files on packs that aren't pure
			if (fs_pure && !searchPath->pack->pure)
				continue;

			// Search inside the pack file
			pack = searchPath->pack;

			for (fileInPack = pack->filesHashTable[hashKey]; fileInPack; fileInPack = fileInPack->nextHash){
				if (fileInPack->isDirectory)
					continue;

				if (!FS_FileNameCompare(fileInPack->name, name)){
					// Found it!
					if (fs_debug->integerValue)
						Com_Printf("FS_OpenFileRead: '%s' (found in '%s')\n", name, pack->name);

					*zipFile = unzOpen(pack->name);
					if (*zipFile){
						if (unzSetOffset(*zipFile, fileInPack->offset) == UNZ_OK){
							if (unzOpenCurrentFile(*zipFile) == UNZ_OK)
								return fileInPack->size;
						}

						unzClose(*zipFile);
					}

					Com_DPrintf(S_COLOR_RED "FS_OpenFileRead: couldn't reopen '%s'\n", pack->name);

					return -1;
				}
			}
		}
		else if (searchPath->directory){
			// If we are pure, only allow certain files from directories
			if (fs_pure){
				if (FS_FileNameCompare(extension, ".cfg") && FS_FileNameCompare(extension, ".demo"))
					continue;
			}

			// Search in the directory tree
			directory = searchPath->directory;

			FS_BuildOSPath(path, sizeof(path), directory->base, directory->game, name);

			*realFile = fopen(path, "rb");
			if (*realFile){
				// Found it!
				if (fs_debug->integerValue)
					Com_Printf("FS_OpenFileRead: '%s' (found in '%s%s%s')\n", name, directory->base, PATH_SEPARATOR_STRING, directory->game);

				return FS_FileLength(*realFile);
			}
		}
	}

	// Not found!
	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileRead: couldn't find '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFileWrite
 ==================
*/
static int FS_OpenFileWrite (const char *name, FILE **realFile){

	char	path[MAX_OSPATH];

	// Make sure the file name is valid
	if (!FS_ValidateFileName(name))
		return -1;

	FS_BuildOSPath(path, sizeof(path), fs_savePath->value, fs_game->value, name);

	// Create any needed subdirectories
	FS_CreateOSPath(path);

	// Open the file
	*realFile = fopen(path, "wb");
	if (*realFile){
		if (fs_debug->integerValue)
			Com_Printf("FS_OpenFileWrite: '%s'\n", name);

		return 0;
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileWrite: couldn't open '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFileAppend
 ==================
*/
static int FS_OpenFileAppend (const char *name, FILE **realFile){

	char	path[MAX_OSPATH];

	// Make sure the file name is valid
	if (!FS_ValidateFileName(name))
		return -1;

	FS_BuildOSPath(path, sizeof(path), fs_savePath->value, fs_game->value, name);

	// Create any needed subdirectories
	FS_CreateOSPath(path);

	// Open the file
	*realFile = fopen(path, "ab");
	if (*realFile){
		if (fs_debug->integerValue)
			Com_Printf("FS_OpenFileAppend: '%s'\n", name);

		return FS_FileLength(*realFile);
	}

	if (fs_debug->integerValue)
		Com_Printf("FS_OpenFileAppend: couldn't open '%s'\n", name);

	return -1;
}

/*
 ==================
 FS_OpenFile
 ==================
*/
int FS_OpenFile (const char *name, fileHandle_t *f, fsMode_t mode){

	file_t	*file;
	FILE	*realFile = NULL;
	unzFile	zipFile = NULL;
	int		size;

	// Try to open the file
	switch (mode){
	case FS_READ:
		size = FS_OpenFileRead(name, &realFile, &zipFile);
		break;
	case FS_WRITE:
		size = FS_OpenFileWrite(name, &realFile);
		break;
	case FS_APPEND:
		size = FS_OpenFileAppend(name, &realFile);
		break;
	default:
		Com_Error(true, "FS_OpenFile: bad mode for '%s'", name);
	}

	if (size == -1){
		*f = 0;

		return -1;
	}

	// Create a new file handle
	file = FS_HandleForFile(f);

	Str_Copy(file->name, name, sizeof(file->name));
	file->mode = mode;
	file->realFile = realFile;
	file->zipFile = zipFile;

	return size;
}

/*
 ==================
 FS_CloseFile
 ==================
*/
void FS_CloseFile (fileHandle_t f){

	file_t	*file;

	file = FS_GetFileByHandle(f);

	if (file->realFile)
		fclose(file->realFile);
	else if (file->zipFile){
		unzCloseCurrentFile(file->zipFile);
		unzClose(file->zipFile);
	}

	memset(file, 0, sizeof(file_t));
}

/*
 ==================
 FS_ReadFile
 ==================
*/
int FS_ReadFile (const char *name, void **buffer){

	fileHandle_t	f;
	byte			*ptr;
	int				size;

	size = FS_OpenFile(name, &f, FS_READ);
	if (!f){
		if (buffer)
			*buffer = NULL;

		return -1;
	}

	if (!buffer){
		FS_CloseFile(f);
		return size;
	}

	ptr = (byte *)Mem_Alloc(size + 1, TAG_TEMP);

	FS_Read(ptr, size, f);
	FS_CloseFile(f);

	ptr[size] = 0;
	*buffer = ptr;

	return size;
}

/*
 ==================
 FS_FreeFile
 ==================
*/
void FS_FreeFile (const void *buffer){

	if (!buffer)
		Com_Error(true, "FS_FreeFile: NULL buffer");

	Mem_Free(buffer);
}

/*
 ==================
 FS_WriteFile
 ==================
*/
bool FS_WriteFile (const char *name, const void *buffer, int size){

	fileHandle_t	f;

	if (!buffer)
		Com_Error(true, "FS_WriteFile: NULL buffer");

	FS_OpenFile(name, &f, FS_WRITE);
	if (!f)
		return false;

	FS_Write(buffer, size, f);
	FS_CloseFile(f);

	return true;
}

/*
 ==================
 FS_CopyFile
 ==================
*/
void FS_CopyFile (const char *srcName, const char *dstName){

	char	srcPath[MAX_OSPATH], dstPath[MAX_OSPATH];
	FILE	*f1, *f2;
	byte	buffer[0x8000];
	int		remaining, count;

	if (fs_debug->integerValue)
		Com_Printf("FS_CopyFile: '%s' to '%s'\n", srcName, dstName);

	// Make sure the file names are valid
	if (!FS_ValidateFileName(srcName) || !FS_ValidateFileName(dstName))
		return;

	FS_BuildOSPath(srcPath, sizeof(srcPath), fs_savePath->value, fs_game->value, srcName);
	FS_BuildOSPath(dstPath, sizeof(dstPath), fs_savePath->value, fs_game->value, dstName);

	// Open the files
	f1 = fopen(srcPath, "rb");
	if (!f1){
		Com_DPrintf(S_COLOR_RED "FS_CopyFile: couldn't open '%s'\n", srcName);
		return;
	}

	f2 = fopen(dstPath, "wb");
	if (!f2){
		fclose(f1);

		Com_DPrintf(S_COLOR_RED "FS_CopyFile: couldn't open '%s'\n", dstName);
		return;
	}

	// Copy in small chunks
	remaining = FS_FileLength(f1);

	while (remaining){
		count = remaining;
		if (count > sizeof(buffer))
			count = sizeof(buffer);

		if (fread(buffer, 1, count, f1) != count){
			Com_DPrintf(S_COLOR_RED "FS_CopyFile: short read from '%s'\n", srcName);
			break;
		}

		if (fwrite(buffer, 1, count, f2) != count){
			Com_DPrintf(S_COLOR_RED "FS_CopyFile: short write to '%s'\n", dstName);
			break;
		}

		remaining -= count;
	}

	fclose(f1);
	fclose(f2);
}

/*
 ==================
 FS_RenameFile
 ==================
*/
void FS_RenameFile (const char *oldName, const char *newName){

	char	oldPath[MAX_OSPATH], newPath[MAX_OSPATH];

	if (fs_debug->integerValue)
		Com_Printf("FS_RenameFile: '%s' to '%s'\n", oldName, newName);

	// Make sure the file names are valid
	if (!FS_ValidateFileName(oldName) || !FS_ValidateFileName(newName))
		return;

	FS_BuildOSPath(oldPath, sizeof(oldPath), fs_savePath->value, fs_game->value, oldName);
	FS_BuildOSPath(newPath, sizeof(newPath), fs_savePath->value, fs_game->value, newName);

	// Rename it
	if (rename(oldPath, newPath))
		Com_DPrintf(S_COLOR_RED "FS_RenameFile: couldn't rename '%s' to '%s'\n", oldName, newName);
}

/*
 ==================
 FS_RemoveFile
 ==================
*/
void FS_RemoveFile (const char *name){

	char	path[MAX_OSPATH];

	if (fs_debug->integerValue)
		Com_Printf("FS_RemoveFile: '%s'\n", name);

	// Make sure the file name is valid
	if (!FS_ValidateFileName(name))
		return;

	FS_BuildOSPath(path, sizeof(path), fs_savePath->value, fs_game->value, name);

	// Remove it
	if (remove(path))
		Com_DPrintf(S_COLOR_RED "FS_RemoveFile: couldn't remove '%s'\n", name);
}

/*
 ==================
 FS_FileExists
 ==================
*/
bool FS_FileExists (const char *name){

	char	path[MAX_OSPATH];
	FILE	*f;

	// Make sure the file name is valid
	if (!FS_ValidateFileName(name))
		return false;

	FS_BuildOSPath(path, sizeof(path), fs_savePath->value, fs_game->value, name);

	// Check if it exists
	f = fopen(path, "rb");
	if (!f)
		return false;

	fclose(f);

	return true;
}


/*
 ==============================================================================

 FILE AND DIRECTORY SCANNING FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 FS_SortFileList
 ==================
*/
static int FS_SortFileList (const void *elem1, const void *elem2){

	const char	*name1 = *(const char **)elem1;
	const char	*name2 = *(const char **)elem2;

	return FS_FileNameCompare(name1, name2);
}

/*
 ==================
 FS_AddFileToList
 ==================
*/
static int FS_AddFileToList (const char *name, const char **files, int fileCount){

	int		i;

	if (fileCount == MAX_LIST_FILES)
		return fileCount;	// Too many files

	// Ignore duplicates
	for (i = 0; i < fileCount; i++){
		if (!FS_FileNameCompare(files[i], name))
			return fileCount;	// Already in list
	}

	// Add it
	files[fileCount++] = Mem_CopyString(name, TAG_TEMP);

	return fileCount;
}

/*
 ==================
 FS_ListFiles
 ==================
*/
const char **FS_ListFiles (const char *path, const char *extension, bool sort, int *numFiles){

	searchPath_t	*searchPath;
	pack_t			*pack;
	fileInPack_t	*fileInPack;
	directory_t		*directory;
	char			name[MAX_OSPATH];
	const char		**sysFileList;
	int				sysNumFiles;
	const char		**fileList;
	const char		*files[MAX_LIST_FILES];
	int				fileCount = 0;
	int				i;

	// Search through the paths, one element at a time
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		if (searchPath->pack){
			// If we are pure, don't search for files on packs that aren't pure
			if (fs_pure && !searchPath->pack->pure)
				continue;

			// Search inside the pack file
			pack = searchPath->pack;

			for (i = 0, fileInPack = pack->files; i < pack->numFiles; i++, fileInPack++){
				// Check the path
				Str_ExtractFilePath(fileInPack->name, name, sizeof(name));

				if (FS_FileNameCompare(path, name))
					continue;

				// Check the extension
				Str_ExtractFileExtension(fileInPack->name, name, sizeof(name));

				if (fileInPack->isDirectory){
					if (extension == NULL || FS_FileNameCompare(extension, "/"))
						continue;
				}
				else {
					if (extension != NULL && FS_FileNameCompare(extension, name))
						continue;
				}

				// Extract the name
				Str_ExtractFileName(fileInPack->name, name, sizeof(name));

				// Add it
				fileCount = FS_AddFileToList(name, files, fileCount);
			}
		}
		else if (searchPath->directory){
			// If we are pure, only allow certain files from directories
			if (fs_pure){
				if (extension == NULL || !FS_FileNameCompare(extension, "/"))
					continue;

				if (FS_FileNameCompare(extension, ".cfg") && FS_FileNameCompare(extension, ".demo"))
					continue;
			}

			// Search in the directory tree
			directory = searchPath->directory;

			Str_SPrintf(name, sizeof(name), "%s/%s/%s", directory->base, directory->game, path);

			// Scan the directory
			sysFileList = Sys_ListFiles(name, extension, false, &sysNumFiles);

			// Add the files
			for (i = 0; i < sysNumFiles; i++)
				fileCount = FS_AddFileToList(sysFileList[i], files, fileCount);

			Sys_FreeFileList(sysFileList);
		}
	}

	if (!fileCount){
		*numFiles = 0;
		return NULL;
	}

	// Sort the list if desired
	if (sort)
		qsort(files, fileCount, sizeof(char *), FS_SortFileList);

	// Copy the list
	fileList = (const char **)Mem_Alloc((fileCount + 1) * sizeof(char *), TAG_TEMP);

	for (i = 0; i < fileCount; i++)
		fileList[i] = files[i];

	fileList[i] = NULL;

	*numFiles = fileCount;

	return fileList;
}

/*
 ==================
 FS_ListFilteredFiles
 ==================
*/
const char **FS_ListFilteredFiles (const char *filter, bool sort, int *numFiles){

	searchPath_t	*searchPath;
	pack_t			*pack;
	fileInPack_t	*fileInPack;
	directory_t		*directory;
	char			name[MAX_OSPATH];
	const char		**sysFileList;
	int				sysNumFiles;
	const char		**fileList;
	const char		*files[MAX_LIST_FILES];
	int				fileCount = 0;
	int				i;

	// Search through the paths, one element at a time
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		if (searchPath->pack){
			// If we are pure, don't search for files on packs that aren't pure
			if (fs_pure && !searchPath->pack->pure)
				continue;

			// Search inside the pack file
			pack = searchPath->pack;

			for (i = 0, fileInPack = pack->files; i < pack->numFiles; i++, fileInPack++){
				// Match filter
				if (!Str_MatchFilter(fileInPack->name, filter, false))
					continue;

				// Add it
				fileCount = FS_AddFileToList(fileInPack->name, files, fileCount);
			}
		}
		else if (searchPath->directory){
			// If we are pure, only allow certain files from directories
			if (fs_pure){
				Str_ExtractFileExtension(filter, name, sizeof(name));

				if (FS_FileNameCompare(name, ".cfg") && FS_FileNameCompare(name, ".demo"))
					continue;
			}

			// Search in the directory tree
			directory = searchPath->directory;

			Str_SPrintf(name, sizeof(name), "%s/%s", directory->base, directory->game);

			// Scan the directory
			sysFileList = Sys_ListFilteredFiles(name, filter, false, &sysNumFiles);

			// Add the files
			for (i = 0; i < sysNumFiles; i++)
				fileCount = FS_AddFileToList(sysFileList[i], files, fileCount);

			Sys_FreeFileList(sysFileList);
		}
	}

	if (!fileCount){
		*numFiles = 0;
		return NULL;
	}

	// Sort the list if desired
	if (sort)
		qsort(files, fileCount, sizeof(char *), FS_SortFileList);

	// Copy the list
	fileList = (const char **)Mem_Alloc((fileCount + 1) * sizeof(char *), TAG_TEMP);

	for (i = 0; i < fileCount; i++)
		fileList[i] = files[i];

	fileList[i] = NULL;

	*numFiles = fileCount;

	return fileList;
}

/*
 ==================
 FS_FreeFileList
 ==================
*/
void FS_FreeFileList (const char **fileList){

	int		i;

	if (!fileList)
		return;

	for (i = 0; fileList[i]; i++)
		Mem_Free(fileList[i]);

	Mem_Free(fileList);
}

/*
 ==================
 FS_SortModList
 ==================
*/
static int FS_SortModList (const void *elem1, const void *elem2){

	const modList_t	*mod1 = *(const modList_t **)elem1;
	const modList_t	*mod2 = *(const modList_t **)elem2;

	return Str_Compare(mod1->description, mod2->description);
}

/*
 ==================
 FS_AddModToList
 ==================
*/
static int FS_AddModToList (const char *name, modList_t **mods, int modCount){

	const char	**sysFileList;
	int			sysNumFiles;
	FILE		*f;
	char		path[MAX_OSPATH];
	char		*description;
	int			len;
	int			i;

	if (modCount == MAX_LIST_MODS)
		return modCount;	// Too many mods

	// Ignore BASE_DIRECTORY
	if (!FS_FileNameCompare(name, BASE_DIRECTORY))
		return modCount;

	// Ignore duplicates
	for (i = 0; i < modCount; i++){
		if (!FS_FileNameCompare(mods[i]->directory, name))
			return modCount;	// Already in list
	}

	// Scan the directory for pack files
	Str_SPrintf(path, sizeof(path), "%s/%s", fs_savePath->value, name);

	sysFileList = Sys_ListFiles(path, ".pk3", false, &sysNumFiles);

	if (!sysNumFiles){
		Str_SPrintf(path, sizeof(path), "%s/%s", fs_basePath->value, name);

		sysFileList = Sys_ListFiles(path, ".pk3", false, &sysNumFiles);

		if (!sysNumFiles)
			return modCount;	// Not a mod
	}

	Sys_FreeFileList(sysFileList);

	// Try to load a description.txt file, otherwise use the directory name
	FS_BuildOSPath(path, sizeof(path), fs_savePath->value, name, "description.txt");

	f = fopen(path, "rt");
	if (!f){
		FS_BuildOSPath(path, sizeof(path), fs_basePath->value, name, "description.txt");

		f = fopen(path, "rt");
	}

	if (f){
		len = FS_FileLength(f);

		description = (char *)Mem_Alloc(len + 1, TAG_TEMP);

		fread(description, 1, len, f);
		description[len] = 0;

		fclose(f);
	}
	else
		description = Mem_CopyString(name, TAG_TEMP);

	// Add it
	mods[modCount] = (modList_t *)Mem_Alloc(sizeof(modList_t), TAG_TEMP);

	mods[modCount]->directory = Mem_CopyString(name, TAG_TEMP);
	mods[modCount]->description = description;

	modCount++;

	return modCount;
}

/*
 ==================
 FS_ListMods
 ==================
*/
modList_t **FS_ListMods (bool sort, int *numMods){

	const char	**sysFileList;
	int			sysNumFiles;
	modList_t	**modList;
	modList_t	*mods[MAX_LIST_MODS];
	int			modCount = 0;
	int			i;

	// Scan the save path
	sysFileList = Sys_ListFiles(fs_savePath->value, "/", false, &sysNumFiles);

	for (i = 0; i < sysNumFiles; i++)
		modCount = FS_AddModToList(sysFileList[i], mods, modCount);

	Sys_FreeFileList(sysFileList);

	// Scan the base path
	sysFileList = Sys_ListFiles(fs_basePath->value, "/", false, &sysNumFiles);

	for (i = 0; i < sysNumFiles; i++)
		modCount = FS_AddModToList(sysFileList[i], mods, modCount);

	Sys_FreeFileList(sysFileList);

	if (!modCount){
		*numMods = 0;
		return NULL;
	}

	// Sort the list if desired
	if (sort)
		qsort(mods, modCount, sizeof(modList_t *), FS_SortModList);

	// Copy the list
	modList = (modList_t **)Mem_Alloc((modCount + 1) * sizeof(modList_t *), TAG_TEMP);

	for (i = 0; i < modCount; i++)
		modList[i] = mods[i];

	modList[i] = NULL;

	*numMods = modCount;

	return modList;
}

/*
 ==================
 FS_FreeModList
 ==================
*/
void FS_FreeModList (modList_t **modList){

	int		i;

	if (!modList)
		return;

	for (i = 0; modList[i]; i++){
		Mem_Free(modList[i]->directory);
		Mem_Free(modList[i]->description);

		Mem_Free(modList[i]);
	}

	Mem_Free(modList);
}


// ============================================================================


/*
 ==================
 FS_LoadZIP

 Creates a pack_t for the contents of a ZIP file
 ==================
*/
static pack_t *FS_LoadZIP (const char *packName){

	pack_t			*pack;
	fileInPack_t	*fileInPack;
	unzFile			handle;
	unz_global_info	header;
	unz_file_info	info;
	ulong			*checksums;
	int				numChecksums;
	char			name[MAX_OSPATH];
	int				len;
	bool			isDirectory;
	uint			hashKey;
	int				i, numFiles;

	handle = unzOpen(packName);
	if (!handle)
		return NULL;

	if (unzGetGlobalInfo(handle, &header) != UNZ_OK){
		unzClose(handle);
		return NULL;
	}

	numFiles = header.number_entry;
	if (numFiles <= 0){
		unzClose(handle);
		return NULL;
	}

	pack = (pack_t *)Mem_Alloc(sizeof(pack_t), TAG_COMMON);
	fileInPack = (fileInPack_t *)Mem_Alloc(numFiles * sizeof(fileInPack_t), TAG_COMMON);

	// Fill it in
	Str_Copy(pack->name, packName, sizeof(pack->name));
	pack->pure = false;
	pack->handle = handle;
	pack->numFiles = numFiles;
	pack->files = fileInPack;

	for (i = 0; i < FILES_HASH_SIZE; i++)
		pack->filesHashTable[i] = NULL;

	checksums = (ulong *)Mem_Alloc(numFiles * sizeof(ulong), TAG_TEMP);
	numChecksums = 0;

	// Parse the directory
	unzGoToFirstFile(handle);

	for (i = 0; i < numFiles; i++, fileInPack++){
		if (unzGetCurrentFileInfo(handle, &info, name, sizeof(name), NULL, 0, NULL, 0) != UNZ_OK)
			break;

		len = Str_Length(name);
		if (name[len-1] == '/'){
			name[len-1] = 0;

			isDirectory = true;
		}
		else {
			checksums[numChecksums++] = LittleLong(info.crc);

			isDirectory = false;
		}

		// Add the file
		Str_Copy(fileInPack->name, name, sizeof(fileInPack->name));
		fileInPack->isDirectory = isDirectory;
		fileInPack->size = info.uncompressed_size;
		fileInPack->offset = unzGetOffset(handle);

		// Add to hash table
		hashKey = Str_HashKey(fileInPack->name, FILES_HASH_SIZE, false);

		fileInPack->nextHash = pack->filesHashTable[hashKey];
		pack->filesHashTable[hashKey] = fileInPack;

		// Go to next file
		unzGoToNextFile(handle);
	}

	// Calculate pack checksum
	pack->checksum = LittleLong(MD4_BlockChecksum(checksums, numChecksums * sizeof(ulong)));

	Mem_Free(checksums);

	return pack;
}

/*
 ==================
 FS_AddGameDirectory

 Adds the directory to the head of the path, then loads and adds all the pack
 files found (in alphabetical order).
 ==================
*/
static void FS_AddGameDirectory (const char *base, const char *game){

	searchPath_t	*searchPath;
	pack_t			*pack;
	directory_t		*directory;
	char			name[MAX_OSPATH];
	const char		**fileList;
	int				numFiles;
	int				i;

	// Don't add the same directory twice
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		if (searchPath->directory){
			directory = searchPath->directory;

			if (!FS_FileNameCompare(directory->base, base) && !FS_FileNameCompare(directory->game, game))
				return;
		}
	}

	// Create a new directory
	directory = (directory_t *)Mem_Alloc(sizeof(directory_t), TAG_COMMON);

	Str_Copy(directory->base, base, sizeof(directory->base));
	Str_Copy(directory->game, game, sizeof(directory->game));

	// Add the directory to the search path
	searchPath = (searchPath_t *)Mem_Alloc(sizeof(searchPath_t), TAG_COMMON);

	searchPath->pack = NULL;
	searchPath->directory = directory;
	searchPath->next = fs_searchPaths;
	fs_searchPaths = searchPath;

	// Scan the directory for pack files
	Str_SPrintf(name, sizeof(name), "%s/%s", base, game);

	fileList = Sys_ListFiles(name, ".pk3", true, &numFiles);

	for (i = 0; i < numFiles; i++){
		FS_BuildOSPath(name, sizeof(name), base, game, fileList[i]);

		// Load it
		pack = FS_LoadZIP(name);
		if (!pack)
			continue;

		// Add the pack file to the search path
		searchPath = (searchPath_t *)Mem_Alloc(sizeof(searchPath_t), TAG_COMMON);

		searchPath->pack = pack;
		searchPath->directory = NULL;
		searchPath->next = fs_searchPaths;
		fs_searchPaths = searchPath;
	}

	Sys_FreeFileList(fileList);
}

/*
 ==================
 FS_SetPurePacks
 ==================
*/
static void FS_SetPurePacks (void){

	searchPath_t	*searchPath;
	int				i;

	if (!fs_pure)
		return;

	// Only pure servers should have this set to false
	if (!fs_purePackChecksumsValid){
		// Mark all packs as pure
		for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
			if (searchPath->pack)
				searchPath->pack->pure = true;
		}

		return;
	}

	// Match the checksums
	for (i = 0; MAX_PACK_FILES; i++){
		if (!fs_purePackChecksums[i])
			break;

		// Run through the paths
		for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
			// Is it a pack file?
			if (!searchPath->pack)
				continue;

			// If the checksum matches, mark it as pure
			if (fs_purePackChecksums[i] == searchPath->pack->checksum){
				searchPath->pack->pure = true;
				break;
			}
		}
	}
}

/*
 ==================
 FS_ReorderPurePacks
 ==================
*/
static void FS_ReorderPurePacks (void){

	searchPath_t	*searchPath, **insert, **previous;
	int				i;

	if (!fs_pure)
		return;

	// Only pure servers should have this set to false
	if (!fs_purePackChecksumsValid)
		return;

	// Reorder the pack files to match the server order
	insert = &fs_searchPaths;

	for (i = 0; MAX_PACK_FILES; i++){
		if (!fs_purePackChecksums[i])
			break;

		previous = insert;

		// Run through the paths
		for (searchPath = *insert; searchPath; searchPath = searchPath->next){
			// Is it a pack file?
			if (!searchPath->pack){
				previous = &searchPath->next;
				continue;
			}

			// If the checksum matches, move this pack file to the insert list
			if (fs_purePackChecksums[i] == searchPath->pack->checksum){
				*previous = searchPath->next;

				searchPath->next = *insert;
				*insert = searchPath;

				insert = &searchPath->next;

				break;
			}

			previous = &searchPath->next;
		}
	}
}

/*
 ==================
 FS_LoadedPackChecksums
 ==================
*/
void FS_LoadedPackChecksums (ulong packChecksums[MAX_PACK_FILES]){

	searchPath_t	*searchPath;
	int				numPackChecksums = 0;

	// Run through the paths
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		// Is it a pack file?
		if (!searchPath->pack)
			continue;

		// If we are pure, only add pure packs to the list
		if (fs_pure && !searchPath->pack->pure)
			continue;

		// Add its checksum to the list
		if (numPackChecksums == MAX_PACK_FILES)
			Com_Error(true, "FS_LoadedPackChecksums: MAX_PACK_FILES hit");

		packChecksums[numPackChecksums++] = searchPath->pack->checksum;
	}

	if (numPackChecksums < MAX_PACK_FILES)
		packChecksums[numPackChecksums] = 0;
}

/*
 ==================
 FS_ComparePackChecksums
 ==================
*/
bool FS_ComparePackChecksums (const ulong packChecksums[MAX_PACK_FILES], ulong missingPackChecksums[MAX_PACK_FILES]){

	searchPath_t	*searchPath;
	int				numMissingPackChecksums = 0;
	int				i;

	// Match the checksums
	for (i = 0; MAX_PACK_FILES; i++){
		if (!packChecksums[i])
			break;

		// Run through the paths
		for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
			// Is it a pack file?
			if (!searchPath->pack)
				continue;

			// If we are pure, only add pure packs to the list
			if (fs_pure && !searchPath->pack->pure)
				continue;

			// If the checksum matches, we have this pack file
			if (packChecksums[i] == searchPath->pack->checksum)
				break;
		}

		// If no matching checksum was found, add it to the missing list
		if (!searchPath){
			if (numMissingPackChecksums == MAX_PACK_FILES)
				Com_Error(true, "FS_ComparePackChecksums: MAX_PACK_FILES hit");

			missingPackChecksums[numMissingPackChecksums++] = packChecksums[i];
		}
	}

	if (numMissingPackChecksums < MAX_PACK_FILES)
		missingPackChecksums[numMissingPackChecksums] = 0;

	return (numMissingPackChecksums != 0);
}

/*
 ==================
 FS_PackNameForChecksum
 ==================
*/
const char *FS_PackNameForChecksum (ulong checksum){

	searchPath_t	*searchPath;

	// Run through the paths
	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		// Is it a pack file?
		if (!searchPath->pack)
			continue;

		// If the checksum matches, this is it
		if (checksum == searchPath->pack->checksum)
			return searchPath->pack->name;
	}

	return NULL;
}

/*
 ==================
 FS_ListFiles_f
 ==================
*/
static void FS_ListFiles_f (void){

	const char	**fileList;
	int			numFiles;
	int			i;

	if (Cmd_Argc() < 2 || Cmd_Argc() > 3){
		Com_Printf("Usage: listFiles <directory> [extension]\n");
		return;
	}

	if (Cmd_Argc() == 2)
		fileList = FS_ListFiles(Cmd_Argv(1), NULL, true, &numFiles);
	else
		fileList = FS_ListFiles(Cmd_Argv(1), Cmd_Argv(2), true, &numFiles);

	for (i = 0; i < numFiles; i++)
		Com_Printf("%s\n", fileList[i]);

	FS_FreeFileList(fileList);

	Com_Printf("--------------------\n");
	Com_Printf("%i files listed\n", numFiles);
}

/*
 ==================
 FS_ListFilteredFiles_f
 ==================
*/
static void FS_ListFilteredFiles_f (void){

	const char	**fileList;
	int			numFiles;
	int			i;

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: listFilteredFiles <filter>\n");
		return;
	}

	fileList = FS_ListFilteredFiles(Cmd_Argv(1), true, &numFiles);

	for (i = 0; i < numFiles; i++)
		Com_Printf("%s\n", fileList[i]);

	FS_FreeFileList(fileList);

	Com_Printf("--------------------\n");
	Com_Printf("%i files listed\n", numFiles);
}

/*
 ==================
 FS_ListActiveHandles_f
 ==================
*/
static void FS_ListActiveHandles_f (void){

	file_t	*file;
	int		handles = 0;
	int		i;

	Com_Printf("Current active handles:\n");

	for (i = 0, file = fs_fileHandles; i < MAX_FILE_HANDLES; i++, file++){
		if (!file->realFile && !file->zipFile)
			continue;

		handles++;

		Com_Printf("%2i ", i+1);

		switch (file->mode){
		case FS_READ:
			Com_Printf("(R) ");
			break;
		case FS_WRITE:
			Com_Printf("(W) ");
			break;
		case FS_APPEND:
			Com_Printf("(A) ");
			break;
		default:
			Com_Printf("(?) ");
			break;
		}

		Com_Printf(": %s\n", file->name);
	}

	Com_Printf("--------------------\n");
	Com_Printf("%i active handles\n", handles);
}

/*
 ==================
 FS_ListSearchPaths_f
 ==================
*/
static void FS_ListSearchPaths_f (void){

	searchPath_t	*searchPath;
	pack_t			*pack;
	directory_t		*directory;
	int				totalFiles = 0, totalPacks = 0;

	Com_Printf("Current search paths:\n");

	for (searchPath = fs_searchPaths; searchPath; searchPath = searchPath->next){
		if (searchPath->pack){
			pack = searchPath->pack;

			Com_Printf("%s (%i files - 0x%08X)\n", pack->name, pack->numFiles, pack->checksum);

			if (fs_pure){
				if (pack->pure)
					Com_Printf("    on the pure list\n");
				else
					Com_Printf("    not on the pure list\n");
			}

			totalFiles += pack->numFiles;
			totalPacks++;
		}
		else if (searchPath->directory){
			directory = searchPath->directory;

			Com_Printf("%s%s%s\n", directory->base, PATH_SEPARATOR_STRING, directory->game);
		}
	}

	Com_Printf("--------------------\n");
	Com_Printf("%i files in %i PK3 files\n", totalFiles, totalPacks);
}

/*
 ==================
 FS_Restart
 ==================
*/
void FS_Restart (bool pure, const ulong purePackChecksums[MAX_PACK_FILES]){

	// Shutdown
	FS_Shutdown();

	// Set pure mode
	fs_pure = pure;

	// Copy the specified pure pack checksums.
	// This should only be NULL for pure servers, or if pure is false.
	if (purePackChecksums){
		fs_purePackChecksumsValid = true;
		memcpy(fs_purePackChecksums, purePackChecksums, MAX_PACK_FILES * sizeof(ulong));
	}
	else {
		fs_purePackChecksumsValid = false;
		memset(fs_purePackChecksums, 0, MAX_PACK_FILES * sizeof(ulong));
	}

	// Initialize
	FS_Init();
}

/*
 ==================
 FS_Init
 ==================
*/
void FS_Init (void){

	Com_Printf("------- File System Initialization -------\n");

	// Allow command line parameters to override our defaults
	Com_StartupVariable("fs_basePath", true);
	Com_StartupVariable("fs_savePath", true);
	Com_StartupVariable("fs_baseGame", true);
	Com_StartupVariable("fs_game", true);
	Com_StartupVariable("fs_debug", true);

	// Register our variables and commands
	fs_basePath = CVar_Get("fs_basePath", Sys_DefaultBaseDirectory(), CVAR_INIT, "Base path");
	fs_savePath = CVar_Get("fs_savePath", Sys_DefaultSaveDirectory(), CVAR_INIT, "Save path");
	fs_baseGame = CVar_Get("fs_baseGame", BASE_DIRECTORY, CVAR_SERVERINFO | CVAR_INIT, "Base game directory");
	fs_game = CVar_Get("fs_game", BASE_DIRECTORY, CVAR_SERVERINFO | CVAR_INIT, "Current game directory");
	fs_debug = CVar_Get("fs_debug", "0", CVAR_CHEAT, "Print debug information");

	Cmd_AddCommand("listFiles", FS_ListFiles_f, "Lists files in a directory");
	Cmd_AddCommand("listFilteredFiles", FS_ListFilteredFiles_f, "Lists files with a filter");
	Cmd_AddCommand("listActiveHandles", FS_ListActiveHandles_f, "Lists active file handles");
	Cmd_AddCommand("listSearchPaths", FS_ListSearchPaths_f, "Lists current search paths");

	// Make sure the variables are valid
	if (!fs_basePath->value[0] || Str_FindChar(fs_basePath->value, '*') || Str_FindChar(fs_basePath->value, '?'))
		CVar_SetString(fs_basePath, Sys_DefaultBaseDirectory());

	if (!fs_savePath->value[0] || Str_FindChar(fs_savePath->value, '*') || Str_FindChar(fs_savePath->value, '?'))
		CVar_SetString(fs_savePath, Sys_DefaultSaveDirectory());

	if (!fs_baseGame->value[0] || Str_FindChar(fs_baseGame->value, '*') || Str_FindChar(fs_baseGame->value, '?') || Str_FindChar(fs_baseGame->value, ' ') || Str_FindChar(fs_baseGame->value, ':') || Str_FindChar(fs_baseGame->value, '/') || Str_FindChar(fs_baseGame->value, '\\') || Str_FindChar(fs_baseGame->value, '.'))
		CVar_SetString(fs_baseGame, BASE_DIRECTORY);

	if (!fs_game->value[0] || Str_FindChar(fs_game->value, '*') || Str_FindChar(fs_game->value, '?') || Str_FindChar(fs_game->value, ' ') || Str_FindChar(fs_game->value, ':') || Str_FindChar(fs_game->value, '/') || Str_FindChar(fs_game->value, '\\') || Str_FindChar(fs_game->value, '.'))
		CVar_SetString(fs_game, BASE_DIRECTORY);

	// Add the directories
	FS_AddGameDirectory(fs_basePath->value, BASE_DIRECTORY);
	FS_AddGameDirectory(fs_savePath->value, BASE_DIRECTORY);

	FS_AddGameDirectory(fs_basePath->value, fs_baseGame->value);
	FS_AddGameDirectory(fs_savePath->value, fs_baseGame->value);

	FS_AddGameDirectory(fs_basePath->value, fs_game->value);
	FS_AddGameDirectory(fs_savePath->value, fs_game->value);

	// If we are pure, set and reorder the pure pack files
	FS_SetPurePacks();

	FS_ReorderPurePacks();

	// Print current search paths
	FS_ListSearchPaths_f();

	// Make sure default.cfg exists
	if (FS_ReadFile("default.cfg", NULL) == -1)
		Com_Error(true, "Could not find default.cfg - Check your installation");

	// Execute config files
	Cmd_AppendText("exec default.cfg\n");
	Cmd_AppendText("exec " ENGINE_NAME ".cfg\n");
	Cmd_AppendText("exec autoexec.cfg\n");
	Cmd_ExecuteBuffer();

	Com_Printf("------------------------------------------\n");
}

/*
 ==================
 FS_Shutdown
 ==================
*/
void FS_Shutdown (void){

	file_t			*file;
	searchPath_t	*searchPath, *next;
	int				i;

	Cmd_RemoveCommand("listFiles");
	Cmd_RemoveCommand("listFilteredFiles");
	Cmd_RemoveCommand("listActiveHandles");
	Cmd_RemoveCommand("listSearchPaths");

	// Close all files
	for (i = 0, file = fs_fileHandles; i < MAX_FILE_HANDLES; i++, file++){
		if (!file->realFile && !file->zipFile)
			continue;

		if (file->realFile)
			fclose(file->realFile);

		if (file->zipFile){
			unzCloseCurrentFile(file->zipFile);
			unzClose(file->zipFile);
		}

		memset(file, 0, sizeof(file_t));
	}

	// Free search paths
	for (searchPath = fs_searchPaths; searchPath; searchPath = next){
		next = searchPath->next;

		if (searchPath->pack){
			if (searchPath->pack->handle)
				unzClose(searchPath->pack->handle);

			Mem_Free(searchPath->pack->files);
			Mem_Free(searchPath->pack);
		}

		if (searchPath->directory)
			Mem_Free(searchPath->directory);

		Mem_Free(searchPath);
	}

	fs_searchPaths = NULL;

	// We are no longer pure
	fs_pure = false;
}
